Aria Tablist
Dependency-free plain JavaScript module for WCAG compliant tablists. Also great for accordions.
Try out the examples
Key design goals and features are:
- multi and single select modes
- horizontal and vertical modes: Adjusts arrow key usage for moving focus between tabs
- progressive enhancement: Allows for only the tab and panel relationship to be indicated in the DOM, and adds
role
and aria
attributes automatically as needed - accessibility: Follows the WCAG spec by default, with options to tweak behaviour
- compatibility: Broad browser and device support (IE9+)
- starting states: Can use
aria-selected="true"
or data-selected="true"
to indicate which tab(s) should be enabled by default. - deletion: Can enable tab (and panel) deletion using the delete key
Installation / usage
Grab from NPM and use in a module system:
npm install aria-tablist
import AriaTablist from 'aria-tablist';
AriaTablist(document.getElementById('tablist'), options);
Or grab the minified JavaScript from unpkg:
<script src="https://unpkg.com/aria-tablist"></script>
The module relies entirely on standard attributes: it sets the role
on elements if it needs to, aria-
attributes for indicating behaviour to screen readers, and relies on setting and removing hidden="hidden"
to toggle element visibility. This means that you can use all of your own class names and styling, and the module won't override them.
HTML Requirements / Progressive Enhancement
When the module is called on an element, the following steps are taken:
- The module will search for
tab
elements using the tabSelector
option ('[role="tab"]'
by default). - If none are found, all direct children will be processed.
- For each assumed
tab
, the module will check for a matching tabpanel
by:
- Checking for an
aria-controls
or data-controls
attribute on the tab
, and searching for an element with a matching id
. - If the
tab
has an id
, searching for an element with an aria-labelledby
or data-labelledby
attribute that matches that id
.
- For any tabs that were processed where a matching panel was not found, if they had
role="tab"
set, the role
attribute will be removed to prevent confusion to screen reader users. - The found tabs and associated panels will then have the relevant
role
and aria-
attributes set automatically.
This means your HTML only needs to indicate the relationship between the tabs and panels, and the module will handle the rest:
<div id="tabs">
<div id="tab-1">Panel 1</div>
<div id="tab-2">Panel 2</div>
<div id="tab-3">Panel 3</div>
</div>
<div data-labelledby="tab-1">...</div>
<div data-labelledby="tab-2">...</div>
<div data-labelledby="tab-3">...</div>
<script>
AriaTablist(document.getElementById('tabs'));
</script>
So if you need to cater for users without JavaScript, or if the JavaScript fails to load for whatever reason, there won't be any applicable roles set that would confuse a screen reader user.
You can of course include all of the optimal ARIA attributes straight away if you wish, including indicating which tab should be active by default:
<div id="tabs" role="tablist" aria-label="Fruits">
<div role="tab" tabindex="-1" aria-controls="panel-1" id="tab-1">
Apple
</div>
<div role="tab" tabindex="0" aria-selected="true" aria-controls="panel-2" id="tab-2">
Orange
</div>
<div role="tab" tabindex="-1" aria-controls="panel-3" id="tab-3">
Pear
</div>
</div>
<div role="tabpanel" aria-labelledby="tab-1" hidden="hidden" id="panel-1">...</div>
<div role="tabpanel" aria-labelledby="tab-2" id="panel-2">...</div>
<div role="tabpanel" aria-labelledby="tab-3" hidden="hidden" id="panel-3">...</div>
Options
Most of the functionality is assumed from the included ARIA attributes in your HTML (see the examples). The remaining available options and their defaults are:
{
delay: number = 0;
deletable: boolean = false;
focusableTabs: boolean = false;
focusablePanels: boolean = true;
arrowActivation: boolean = false;
allArrows: boolean = false;
tabSelector: string = '[role="tab"]';
tabindex: number | string = 0;
onOpen: (panel: HTMLElement, tab: HTMLElement) => void;
onClose: (panel: HTMLElement, tab: HTMLElement) => void;
onDelete: (tab: HTMLElement) => void;
onReady: (tablist: HTMLElement) => void;
}
All component options that accept a Function will have their context (this
) set to include the full autocomplete API (assuming you use a normal function: () {}
declaration for the callbacks instead of arrow functions).
API
The returned AriaTablist
class instance exposes the following API (which is also available on the original element's ariaTablist
property):
{
tabs: HTMLElement[];
panels: HTMLElement[];
options: AriaTablistOptions;
open(index: number | HTMLElement, focusTab: boolean = true): void;
close(index: number | HTMLElement, focusTab: boolean = false): void;
delete(index: number | HTMLElement): void;
destroy(): void;
}